/*
 * Iparelan ExcepGen
 * This file is part of ExcepGen.
 * Copyright (c) 2008, 2009, Iparelan Solutions, LLC. All rights reserved.
 *
 * ExcepGen is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * ExcepGen is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ExcepGen. If not, see <http://www.gnu.org/licenses/>.
 */

package com.iparelan.excepgen;

import java.io.File;
import java.io.IOException;

import java.util.Collection;

import java.util.logging.Logger;

import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import static com.iparelan.excepgen.L7dMsg.MSGS;

/**
 * Converts a specification for Java exception types into Java source code.
 *
 * @author Greg Mattes
 * @version September 2008
 *
 * @to.do
 *
 * <ul>
 *
 * <li>XML schema for configuration file validation.
 *
 * <li>Create UML graph/diagram to show flow of application, add to this
 *     documentation.
 *
 * <li>Generate build dependencies for inclusion in build script, e.g. Ant.
 *
 * </ul>
 */
@edu.umd.cs.findbugs.annotations.SuppressWarnings(
    value = "DM_EXIT",
    justification = "script fatal error situation"
)
public final class ExcepGen implements Runnable {
    /** The string that terminates lines of output. */
    public static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n");

    /** The ExcepGen {@link java.util.logging.Logger}. */
    private static final Logger LOGGER = Logger.getLogger(ExcepGen.class.getSimpleName());

    /** The ExcepGen configuration file. */
    private final File configFile;

    /** The directory into which generated Java source code is written. */
    private final File targetDirectory;

    /**
     * Not externally instantiable.
     *
     * @param configFilename
     *
     *        The name of the ExcepGen configuration file. May not be {@code null}.
     *
     * @param targetDirectoryName
     *
     *        The name of the directory in which generated Java source code is
     *        written. May not be {@code null}.
     *
     * @throws IOException
     *
     *         Iff a readable configuration file is not found or a writable
     *         directory is not found or cannot be created.
     */
    private ExcepGen(final String configFilename, final String targetDirectoryName) throws IOException {
        this.configFile = getConfigFile(configFilename);
        this.targetDirectory = getTargetDirectory(targetDirectoryName);
    }

    /**
     * Program entry point.
     *
     * @param args
     *
     *        The command line arguments. Never {@code null}.  There must be
     *        two command line arguments: the configuration filename and
     *        the target directory.
     */
    public static void main(final String... args) {
        try {
            final boolean improperArgCount = (args.length != 2);
            if (improperArgCount) {
                final String errMsg = MSGS.get(L7dMsg.USAGE);
                LOGGER.log(SEVERE, errMsg);
            } else {
                final String logMsg = MSGS.get(L7dMsg.ARG_COUNT_OK);
                LOGGER.log(FINE, logMsg);
                final String configFilename = args[0];
                final String targetDirectoryName = args[1];
                final ExcepGen excepGen = new ExcepGen(configFilename, targetDirectoryName);
                excepGen.run();
            }
        } catch (final IOException ioe) {
            final String errMsg = MSGS.get(L7dMsg.IO_ERROR);
            LOGGER.log(SEVERE, errMsg, ioe);
        }
    }

    @Override
    @SuppressWarnings("PMD.AvoidCatchingThrowable") // justification: top level catch block
    // BEGIN SuppressWarnings(Checkstyle.IllegalCatch) justification: top level catch block
    public void run() {
        try {
            final String threadName = MSGS.get(L7dMsg.APP_THREAD_NAME);
            Thread.currentThread().setName(threadName);

            // Create SAX parser
            final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
            final SAXParser parser = parserFactory.newSAXParser();
            final String saxParserOKLogMsg = MSGS.get(L7dMsg.SAX_PARSER_OK);
            LOGGER.log(FINE, saxParserOKLogMsg);

            // Create configuration SAX event handler
            final ConfigHandler configHandler = new ConfigHandler();
            final String saxHandlerOKLogMsg = MSGS.get(L7dMsg.SAX_HANDLER_OK);
            LOGGER.log(FINE, saxHandlerOKLogMsg);

            // Parse the configuration file
            parser.parse(configFile, configHandler);

            // Generate code
            final Collection<ExceptionSpecification> exceptionSpecifications = configHandler.getExceptionSpecifications();
            for (ExceptionSpecification exceptionSpecification : exceptionSpecifications) {
                final String noticeMsg = MSGS.get(L7dMsg.CODE_GENERATION_NOTICE, exceptionSpecification);
                LOGGER.log(FINE, noticeMsg);
                exceptionSpecification.write(targetDirectory);
            }
        } catch (final SAXParseException spe) {
            final String parseErrMsg = spe.getMessage();
            final String file = spe.getSystemId();
            final int lineNumber = spe.getLineNumber();
            final int columnNumber = spe.getColumnNumber();
            final Throwable cause = spe.getException();
            final String errMsg = MSGS.get(L7dMsg.SAX_PARSE_ERROR, parseErrMsg, file, lineNumber, columnNumber);
            LOGGER.log(SEVERE, errMsg + LINE_SEPARATOR + cause);
        } catch (final SAXException se) {
            final String errMsg = MSGS.get(L7dMsg.SAX_ERROR);
            LOGGER.log(SEVERE, errMsg, se);
        } catch (final Throwable t) {
            final String errMsg = MSGS.get(L7dMsg.STRANGE_EXCEPTION);
            LOGGER.log(SEVERE, errMsg, t);
        }
    }
    // END SuppressWarnings(Checkstyle.IllegalCatch)

    /**
     * Produces a {@link java.util.File} that represents a readble ExcepGen
     * configuration file.
     *
     * @param configFilename
     *
     *        The name of the ExcepGen configuration file. May not be {@code null}.
     *
     * @return A {@code File} that represents a readble ExcepGen configuration
     *         file. Never {@code null}.
     *
     * @throws IOException
     *
     *         Iff a readable configuration file is not found.
     */
    private File getConfigFile(final String configFilename) throws IOException {
        final File file = new File(configFilename);
        final boolean badConfigFile = (! (file.isFile() && file.canRead()));
        if (badConfigFile) {
            final String errMsg = MSGS.get(L7dMsg.CONFIG_BAD, file);
            throw new IOException(errMsg);
        }
        final String logMsg = MSGS.get(L7dMsg.CONFIG_OK, file);
        LOGGER.log(FINE, logMsg);
        return file;
    }

    /**
     * Produces a {@link java.util.File} that represents a directory into
     * which generated Java source code is placed.
     *
     * @param targetDirectoryName
     *
     *        The name of the directory in which generated Java source code is
     *        written. May not be {@code null}.
     *
     * @return A {@code File} that represents a writable directory. Never
     *         {@code null}.
     *
     * @throws IOException
     *
     *         Iff a writable directory is not found or cannot be created.
     */
    private File getTargetDirectory(final String targetDirectoryName) throws IOException {
        final File directory = new File(targetDirectoryName);

        final boolean directoryNotThere = (! directory.exists());
        if (directoryNotThere) {
            final boolean couldNotCreateDirectory = (! directory.mkdir());
            if (couldNotCreateDirectory) {
                final String errMsg = MSGS.get(L7dMsg.DIRECTORY_CANNOT_BE_CREATED, directory);
                throw new IOException(errMsg);
            }
        }

        final boolean notDirectory = (! directory.isDirectory());
        if (notDirectory) {
            final String errMsg = MSGS.get(L7dMsg.NOT_DIRECTORY, directory);
            throw new IOException(errMsg);
        }

        final boolean targetDirectoryNotWritable = (! directory.canWrite());
        if (targetDirectoryNotWritable) {
            final String errMsg = MSGS.get(L7dMsg.DIRECTORY_NOT_WRITABLE, directory);
            throw new IOException(errMsg);
        }

        final String logMsg = MSGS.get(L7dMsg.DIRECTORY_WRITABLE, directory);
        LOGGER.log(FINE, logMsg);

        return directory;
    }
}
